package com.google.zxing.journeyapps.barcodescanner.camera;

import com.google.zxing.journeyapps.barcodescanner.BusHelper;
import com.google.zxing.journeyapps.barcodescanner.CameraPreview;
import com.google.zxing.journeyapps.barcodescanner.Size;
import com.google.zxing.journeyapps.barcodescanner.Util;
import ohos.agp.graphics.Surface;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Manage a camera instance using a background thread.
 *
 * All methods must be called from the main thread.
 */
public class CameraInstance implements BusHelper.Observable {
    private static final String TAG = CameraInstance.class.getSimpleName();

    private CameraThread cameraThread;
    private CameraSurface surface;

    private CameraManager cameraManager;
    private EventHandler readyHandler;
    private DisplayConfiguration displayConfiguration;
    private boolean open = false;
    private boolean cameraClosed = true;
    private EventHandler mainHandler;

    private CameraSettings cameraSettings = new CameraSettings();

    /**
     * Construct a new CameraInstance.
     *
     * A new CameraManager is created.
     *
     * @param context
     */
    public CameraInstance(Context context) {
        Util.validateMainThread();

        this.cameraThread = CameraThread.getInstance();
        this.cameraManager = new CameraManager(context);
        this.cameraManager.setCameraSettings(cameraSettings);
        this.mainHandler = new EventHandler(EventRunner.getMainEventRunner());
    }

    /**
     * Construct a new CameraInstance with a specific CameraManager.
     *
     * @param cameraManager the CameraManager to use
     */
    public CameraInstance(CameraManager cameraManager) {
        Util.validateMainThread();

        this.cameraManager = cameraManager;
    }

    public void setDisplayConfiguration(DisplayConfiguration configuration) {
        this.displayConfiguration = configuration;
        cameraManager.setDisplayConfiguration(configuration);
    }

    public DisplayConfiguration getDisplayConfiguration() {
        return displayConfiguration;
    }

    public void setReadyHandler(EventHandler readyHandler) {
        this.readyHandler = readyHandler;
    }

    public void setSurfaceHolder(Surface surface) {
        setSurface(new CameraSurface(surface));
    }

    public void setSurface(CameraSurface surface) {
        this.surface = surface;
    }

    public CameraSettings getCameraSettings() {
        return cameraSettings;
    }

    /**
     * This only has an effect if the camera is not opened yet.
     *
     * @param cameraSettings the new camera settings
     */
    public void setCameraSettings(CameraSettings cameraSettings) {
        if (!open) {
            this.cameraSettings = cameraSettings;
            this.cameraManager.setCameraSettings(cameraSettings);
        }
    }

    /**
     * Actual preview size in current rotation. null if not determined yet.
     *
     * @return preview size
     */
    private Size getPreviewSize() {
        return cameraManager.getPreviewSize();
    }

    /**
     *
     * @return the camera rotation relative to display rotation, in degrees. Typically 0 if the
     *    display is in landscape orientation.
     */
    public int getCameraRotation() {
        return cameraManager.getCameraRotation();
    }

    public void open() {
        Util.validateMainThread();
        cameraClosed = false;
        cameraThread.incrementAndEnqueue(opener);
        BusHelper.getINSTANCE().register("cameraInitCompleted",this);
    }

    public void configureCamera() {
        Util.validateMainThread();
        Logger.getLogger(TAG).warning("configureCamera open:" + open);
        if (!open)
            return;
        cameraThread.enqueue(configure);
    }

    public void startPreview() {
        Util.validateMainThread();
        validateOpen();
        cameraThread.enqueue(previewStarter);
    }

    public void setTorch(final boolean on) {
        Util.validateMainThread();
        if (open) {
            cameraThread.enqueue(() -> cameraManager.setTorch(on));
        }
    }

    /**
     * Changes the settings for Camera.
     *
     * @param callback {@link CameraParametersCallback}
     */
    public void changeCameraParameters(final CameraParametersCallback callback) {
        Util.validateMainThread();
        if (open) {
            cameraThread.enqueue(() -> cameraManager.changeCameraParameters(callback));
        }
    }

    public void close() {
        Util.validateMainThread();
        if (open) {
            cameraThread.enqueue(closer);
        } else {
            cameraClosed = true;
        }
        open = false;
    }

    public boolean isOpen() {
        return open;
    }

    public boolean isCameraClosed() {
        return cameraClosed;
    }

    public void requestPreview(final PreviewCallback callback) {
        mainHandler.postTask(() -> {
            if (!open) {
                Logger.getLogger(TAG).config("Camera is closed, not requesting preview");
                return;
            }
            cameraThread.enqueue(() -> cameraManager.requestPreviewFrame(callback));
        });
    }

    private void validateOpen() {
        if (!open) {
            throw new IllegalStateException("CameraInstance is not open");
        }
    }

    private Runnable opener = new Runnable() {
        @Override
        public void run() {
            try {
                Logger.getLogger(TAG).warning("Opening camera");
                cameraManager.open();
            } catch (Exception e) {
                notifyError(e);
                Logger.getLogger(TAG).log(Level.SEVERE,"Failed to open camera", e);
            }
        }
    };

    @Override
    public void onSubscribe(Object object) {
        open = true;
        BusHelper.getINSTANCE().unRegister(this);
    }

    private Runnable configure = new Runnable() {
        @Override
        public void run() {
            try {
                cameraManager.configure();
                if (readyHandler != null) {
                    readyHandler.sendEvent(InnerEvent.get(CameraPreview.zxing_prewiew_size_ready, getPreviewSize()));
                }
            } catch (Exception e) {
                notifyError(e);
                Logger.getLogger(TAG).log(Level.SEVERE,"Failed to configure camera", e);
            }
        }
    };

    private Runnable previewStarter = new Runnable() {
        @Override
        public void run() {
            try {
                Logger.getLogger(TAG).warning("Starting preview - ");
                cameraManager.setPreviewDisplay(surface);
            } catch (Exception e) {
                notifyError(e);
                Logger.getLogger(TAG).log(Level.SEVERE,"Failed to start preview", e);
            }
        }
    };

    private Runnable closer = new Runnable() {
        @Override
        public void run() {
            try {
                Logger.getLogger(TAG).config("Closing camera");
                cameraManager.stopPreview();
                cameraManager.close();
            } catch (Exception e) {
                Logger.getLogger(TAG).log(Level.SEVERE,"Failed to close camera", e);
            }

            cameraClosed = true;

            readyHandler.sendEvent(CameraPreview.zxing_camera_closed);

            cameraThread.decrementInstances();
        }
    };

    private void notifyError(Exception error) {
        if (readyHandler != null) {
            readyHandler.sendEvent(InnerEvent.get(CameraPreview.zxing_camera_error, error));
        }
    }

    /**
     * Returns the CameraManager used to control the camera.
     *
     * The CameraManager is not thread-safe, and must only be used from the CameraThread.
     *
     * @return the CameraManager used
     */
    protected CameraManager getCameraManager() {
        return cameraManager;
    }

    /**
     *
     * @return the CameraThread used to manage the camera
     */
    protected CameraThread getCameraThread() {
        return cameraThread;
    }

    /**
     *
     * @return the surface om which the preview is displayed
     */
    protected CameraSurface getSurface() {
        return surface;
    }
}
